﻿/*
   Copyright 2010 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

/*
 * Computed Tomography voxel set stream handler
 * 
 * Allows a "virtual" subset of the voxel data to be extracted as a stream
 * 
 * Dr. Richard Boardman
 * Engineering Materials Group
 * School of Engineering Sciences
 * Copyright (c) 2010 University of Southampton
 *
 */

using System;
using System.IO;

namespace uk.ac.soton.ses   
{
    /// <summary>
    /// Allows a "virtual" subset of the voxel data to be extracted as a stream. Note that the performance of
    /// this can vary depending on the strides required on the underlying dataset. 
    /// 
    /// Of particular note is that attempting to seek through this stream may incur heavy performance penalties
    /// if the seek results always miss the buffer cache, for example, seeking to a position 
    /// <code>bufferSize</code>+1 bytes ahead, reading one byte, and seeking again to <code>bufferSize</code>+1
    /// bytes ahead.
    /// 
    /// When initialised, the default buffer size will be the size of the real data's XY plane in voxels, which 
    /// should be a sensible compromise. If the XY plane is larger than the maximum allowable buffer size, then 
    /// the buffer size will be set to the maximum allowable. Note that this means if the underlying data depth
    /// is 8-bit, the buffer will hold an entire plane of voxels. If it is 16-bit, it will be half this, and half
    /// again for 32-bit.
    /// </summary>
    public class CtReaderStream : Stream
    {
        /// <summary>
        /// The low crop point in the X direction
        /// </summary>
        private long low_x = -1;

        /// <summary>
        /// The low crop point in the Y direction
        /// </summary>
        private long low_y = -1;

        /// <summary>
        /// The low crop point in the Z direction
        /// </summary>
        private long low_z = -1;

        /// <summary>
        /// The high crop point in the X direction
        /// </summary>
        private long high_x = -1;

        /// <summary>
        /// The high crop point in the Y direction
        /// </summary>
        private long high_y = -1;

        /// <summary>
        /// The high crop point in the Z direction
        /// </summary>
        private long high_z = -1;

        /// <summary>
        /// The computed length of the virtual X dimension
        /// </summary>
        private long virtual_x_length = -1;

        /// <summary>
        /// The computed length of the virtual Y dimension
        /// </summary>
        private long virtual_y_length = -1;

        /// <summary>
        /// The computed length of the virtual Z dimension
        /// </summary>
        private long virtual_z_length = -1;

        /// <summary>
        /// The computed size of the virtual X-Y plane
        /// </summary>
        private long virtual_xy_plane_size = -1;

        /// <summary>
        /// The <code>CtReader</code> provided size of the real X dimension
        /// </summary>
        private long real_x_length = -1;

        /// <summary>
        /// The <code>CtReader</code> provided size of the real Y dimension
        /// </summary>
        private long real_y_length = -1;

        /// <summary>
        /// The <code>CtReader</code> provided size of the real Z dimension
        /// </summary>
        private long real_z_length = -1;

        /// <summary>
        /// The computed size of the real X-Y plane
        /// </summary>
        private long real_xy_plane_size = -1;

        /// <summary>
        /// The provided <code>CtReader</code> containing the real dimensions and source data file
        /// </summary>
        private CtReader ctr = null;

        /// <summary>
        /// The total length of the virtual stream
        /// </summary>
        private long length = -1;

        /// <summary>
        /// The number of bytes per element in the stream
        /// </summary>
        private int bytesPerVoxel = -1;

        /// <summary>
        /// The computed position in the virtual stream. See method <see cref="GetRealPosition"/>
        /// </summary>
        private long position = -1;

        /// <summary>
        /// The underlying <code>BinaryReader</code> to which this stream interfaces
        /// </summary>
        private BinaryReader br = null;

        /// <summary>
        /// The maximum buffer size for reading. The internal buffer should never exceed this size
        /// </summary>
        internal int maxBufferSize = 100 * 1024 * 1024; // no more than 100M

        /// <summary>
        /// The default internal buffer size. This will be set in the <code>Initialise</code> block,
        /// or overridden with a smaller block in the case of approaching the end of the real stream
        /// </summary>
        private int internalBufferSize = 10 * 1024 * 1024; 

        /// <summary>
        /// The last real position of the 0th byte of the <code>internalBuffer</code>
        /// </summary>
        private long lastBufferRealPosition = -1;

        /// <summary>
        /// The internal buffer for the real stream
        /// </summary>
        private byte[] internalBuffer = null;        

        /// <summary>
        /// Creates a new stream to the underlying data in the <code>CtReader</code>
        /// </summary>
        /// <param name="ctr">The <code>CtReader</code> providing the data</param>
        public CtReaderStream(CtReader ctr)
        {
            this.ctr = ctr;

            this.low_x = 0;
            this.low_y = 0;
            this.low_z = 0;

            this.high_x = ctr.GetXSize();
            this.high_y = ctr.GetYSize();
            this.high_z = ctr.GetZSize();

            this.Initialise();
        }

        /// <summary>
        /// Creates a new stream to the underlying data in the <code>CtReader</code>
        /// </summary>
        /// <param name="ctr">The <code>CtReader</code> providing the data</param>
        /// <param name="low_x">The low crop point in the X direction</param>
        /// <param name="low_y">The low crop point in the Y direction</param>
        /// <param name="low_z">The low crop point in the Z direction</param>
        /// <param name="high_x">The high crop point in the X direction</param>
        /// <param name="high_y">The high crop point in the Y direction</param>
        /// <param name="high_z">The high crop point in the Z direction</param>
        public CtReaderStream(CtReader ctr, long low_x, long low_y, long low_z, long high_x, long high_y, long high_z)
        {
            this.ctr = ctr;

            this.low_x = low_x;
            this.low_y = low_y;
            this.low_z = low_z;

            this.high_x = high_x;
            this.high_y = high_y;
            this.high_z = high_z;

            this.Initialise();                         
        }

        /// <summary>
        /// Initialises the <code>CtReaderStream</code> values. Should be called after any constructor
        /// </summary>
        private void Initialise()
        {
            if (this.high_z < this.low_z)
            {
                throw new CtReaderException("High Z is greater than low Z");
            }
            if (this.high_y < this.low_y)
            {
                throw new CtReaderException("High Z is greater than low Z");
            }
            if (this.high_x < this.low_x)
            {
                throw new CtReaderException("High Z is greater than low Z");
            }            

            switch (this.ctr.GetDepth())
            {
                case 8:
                    this.bytesPerVoxel = 1;
                    break;
                case 16:
                    this.bytesPerVoxel = 2;
                    break;
                case 32:
                    this.bytesPerVoxel = 4;
                    break;
                default:
                    throw new CtReaderException("Data depth is not one of 8-bit, 16-bit or 32-bit");
            }

            // precompute some values
            this.virtual_x_length = this.high_x - this.low_x;
            this.virtual_y_length = this.high_y - this.low_y;
            this.virtual_z_length = this.high_z - this.low_z;

            this.virtual_xy_plane_size = this.virtual_x_length * this.virtual_y_length;

            // avoid unnecessary method calls
            this.real_x_length = this.ctr.GetXSize();
            this.real_y_length = this.ctr.GetYSize();
            this.real_z_length = this.ctr.GetZSize();

            this.real_xy_plane_size = this.real_x_length * this.real_y_length;

            this.length = this.virtual_x_length * this.virtual_y_length * this.virtual_z_length;

            this.position = 0;

            this.br = new BinaryReader(File.Open(this.ctr.sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read));

            // ensure the buffer size is close to an XY plane, but not larger than the maximum buffer size
            if (this.real_xy_plane_size < this.maxBufferSize)
            {
                this.internalBufferSize = (int)this.real_xy_plane_size;
            }
            else
            {
                // subtract one, two or four bytes from the internal buffer to ensure
                // the buffer size sanity check does not exceed the maximum buffer size
                this.internalBufferSize = this.maxBufferSize - this.bytesPerVoxel;
            }

            // sanity check the buffer size: buffer size should be a multiple of bytesperread
            int internalBufferModulo = this.internalBufferSize % this.bytesPerVoxel;
            if (internalBufferModulo != 0)
            {
                this.internalBufferSize += (this.bytesPerVoxel - internalBufferModulo);
            }
            
            // initialise the buffer
            this.internalBuffer = new byte[internalBufferSize];
        }

        /// <summary>
        /// The number of bytes that should be read per read operation
        /// </summary>
        public int BytesPerVoxel
        {
            get { return this.bytesPerVoxel; }
        }

        /// <summary>
        /// Whether the stream can be read
        /// </summary>
        public override bool CanRead
        {
            get { return true; }
        }

        /// <summary>
        /// Whether the stream can be "seeked"        
        /// </summary>
        public override bool CanSeek
        {
            get { return true; }
        }

        /// <summary>
        /// Whether the stream can be written to
        /// </summary>
        public override bool CanWrite
        {
            get { return false; }
        }

        /// <summary>
        /// Flushes the base stream
        /// </summary>
        public override void Flush()
        {
            this.br.BaseStream.Flush();            
        }

        /// <summary>
        /// The length of this virtual stream
        /// </summary>
        public override long Length
        {
            get { return this.length; }            
        }

        /// <summary>
        /// The current position in this virtual stream
        /// </summary>
        public override long Position
        { 
            get { return this.position; }
            set { this.position = value;  }
        }

        /// <summary>
        /// The associated <code>CtReader</code> for this stream
        /// </summary>
        public CtReader CtReader
        {
            get { return this.ctr; }
        }

        /// <summary>
        /// Translates the virtual position into the real position in the underlying data stream.
        /// Note that it is this method that sets the position to the requested virtual position *plus 1*;
        /// this is to help calling methods ensure the position is advanced when moving through the
        /// stream with a ReadFoo(void) method
        /// </summary>
        /// <param name="virtualPosition">The position in the virtual stream</param>
        /// <returns>The equivalent position in the real stream</returns>
        private long GetRealPosition(long virtualPosition)
        {            
            // increment the virtual position for incremental reads
            this.position = virtualPosition + 1;            

            // translate incoming virtual position to virtual coordinates
            long vz = virtualPosition / this.virtual_xy_plane_size;
            long vy = (virtualPosition - (vz * this.virtual_xy_plane_size)) / this.virtual_x_length;
            long vx = (virtualPosition - (vz * this.virtual_xy_plane_size)) % this.virtual_x_length;

            // translate virtual coordinates to real coordinates
            long rz = vz + this.low_z;
            long ry = vy + this.low_y;
            long rx = vx + this.low_x;

            // translate real coordinates to real position
            long realPosition = rx + (ry * this.real_x_length) + (rz * this.real_xy_plane_size);            

            // return real position
            return realPosition * this.bytesPerVoxel;
        }

        /// <summary>
        /// Fills the buffer with data from position <code>realPosition</code> in the underlying
        /// data stream. If the buffer is larger than the remaining data, then the internal
        /// buffer size is reduced to this remainder. Note that this has possible performance issues
        /// if the end of the stream is approached, the buffer is filled, then the stream is read
        /// again as the buffer will be smaller.
        /// </summary>
        /// <param name="realPosition">The real position in the stream from which to fill the buffer</param>
        private void fillBuffer(long realPosition)
        {           
            this.br.BaseStream.Seek(realPosition, SeekOrigin.Begin);
            if (this.internalBufferSize + realPosition >= this.br.BaseStream.Length)
            {
                this.internalBufferSize = (int)(this.br.BaseStream.Length - realPosition);
            }
            this.internalBuffer = this.br.ReadBytes(this.internalBufferSize);                
            this.lastBufferRealPosition = realPosition;            
        }
        
        /// <summary>
        /// Calls <code>fillBuffer</code> if the requested byte is out of bounds of the current buffer, or
        /// the buffer has never been filled (i.e., <code>lastBufferRealPosition</code> == 1)
        /// </summary>
        /// <param name="realPosition">The real position in the stream to check</param>
        private void fillBufferIfNecessary(long realPosition)
        {            
            if (this.internalBufferSize <= (realPosition - this.lastBufferRealPosition) || this.lastBufferRealPosition < 0)
            {
                this.fillBuffer(realPosition);
            }
        }

        /// <summary>
        /// Retrieves a byte from the buffer at <code>realPosition - lastBufferRealPosition</code>, rebuffering
        /// if this is required
        /// </summary>
        /// <param name="realPosition">The real position for which to retrieve the value</param>
        /// <returns>Voxel value</returns>
        private byte getByteFromBuffer(long realPosition)
        {
            this.fillBufferIfNecessary(realPosition);
            return this.internalBuffer[realPosition - this.lastBufferRealPosition];
        }

        /// <summary>
        /// Retrieves a ushort from the buffer at <code>realPosition - lastBufferRealPosition</code>, rebuffering
        /// if this is required
        /// </summary>
        /// <param name="realPosition">The real position for which to retrieve the value</param>
        /// <returns>Voxel value</returns>
        private ushort getUshortFromBuffer(long realPosition)
        {
            this.fillBufferIfNecessary(realPosition);
            return BitConverter.ToUInt16(this.internalBuffer, (int)(realPosition - this.lastBufferRealPosition));
        }

        /// <summary>
        /// Retrieves a uint from the buffer at <code>realPosition - lastBufferRealPosition</code>, rebuffering
        /// if this is required
        /// </summary>
        /// <param name="realPosition">The real position for which to retrieve the value</param>
        /// <returns>Voxel value</returns>
        private uint getUintFromBuffer(long realPosition)
        {
            this.fillBufferIfNecessary(realPosition);
            return BitConverter.ToUInt32(this.internalBuffer, (int)(realPosition - this.lastBufferRealPosition));
        }

        /// <summary>
        /// Retrieves a short from the buffer at <code>realPosition - lastBufferRealPosition</code>, rebuffering
        /// if this is required
        /// </summary>
        /// <param name="realPosition">The real position for which to retrieve the value</param>
        /// <returns>Voxel value</returns>
        private short getShortFromBuffer(long realPosition)
        {
            this.fillBufferIfNecessary(realPosition);
            return BitConverter.ToInt16(this.internalBuffer, (int)(realPosition - this.lastBufferRealPosition));
        }

        /// <summary>
        /// Retrieves an int from the buffer at <code>realPosition - lastBufferRealPosition</code>, rebuffering
        /// if this is required
        /// </summary>
        /// <param name="realPosition">The real position for which to retrieve the value</param>
        /// <returns>Voxel value</returns>
        private int getIntFromBuffer(long realPosition)
        {
            this.fillBufferIfNecessary(realPosition);
            return BitConverter.ToInt32(this.internalBuffer, (int)(realPosition - this.lastBufferRealPosition));
        }

        /// <summary>
        /// Reads one byte from the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        public byte ReadByte(long virtualPosition)        
        {
            return this.getByteFromBuffer(this.GetRealPosition(virtualPosition));            
        }

        /// <summary>
        /// Original unbuffered method to read one byte
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>        
        private byte UnbufferedReadByte(long virtualPosition)
        {            
            this.br.BaseStream.Seek(this.GetRealPosition(virtualPosition), SeekOrigin.Begin);
            return this.br.ReadByte();            
        }

        /// <summary>
        /// Reads the byte at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        public new byte ReadByte()
        {            
            return this.getByteFromBuffer(this.GetRealPosition(this.position));
        }

        /// <summary>
        /// Original unbuffered method to read the byte at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        private byte UnbufferedReadByte()
        {
            long realpos = this.GetRealPosition(this.position);
            this.br.BaseStream.Seek(realpos, SeekOrigin.Begin);
            return this.br.ReadByte();
        }

        /// <summary>
        /// Reads the unsigned short at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        public ushort ReadUInt16(long virtualPosition)
        {
            return this.getUshortFromBuffer(this.GetRealPosition(virtualPosition));
        }

        /// <summary>
        /// Original unbuffered method to read the unsigned short at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        private ushort UnbufferedReadUInt16(long virtualPosition)
        {
            this.br.BaseStream.Seek(this.GetRealPosition(virtualPosition), SeekOrigin.Begin);
            return this.br.ReadUInt16();
        }

        /// <summary>
        /// Reads the unsigned short at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        public ushort ReadUInt16()
        {
            return this.getUshortFromBuffer(this.GetRealPosition(this.position));
        }

        /// <summary>
        /// Original unbuffered method to read the unsigned short at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        private ushort UnbufferedReadUInt16()
        {
            this.br.BaseStream.Seek(this.GetRealPosition(this.position), SeekOrigin.Begin);            
            return this.br.ReadUInt16();
        }

        /// <summary>
        /// Reads the unsigned integer at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        public uint ReadUInt32(long virtualPosition)
        {
            return this.getUintFromBuffer(this.GetRealPosition(virtualPosition));
        }

        /// <summary>
        /// Original unbuffered method to read the unsigned integer at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        private uint UnbufferedReadUInt32(long virtualPosition)
        {
            this.br.BaseStream.Seek(this.GetRealPosition(virtualPosition), SeekOrigin.Begin);
            return this.br.ReadUInt32();            
        }

        /// <summary>
        /// Reads the unsigned integer at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        public uint ReadUInt32()
        {
            return this.getUintFromBuffer(this.GetRealPosition(this.position));
        }

        /// <summary>
        /// Original unbuffered method to read the unsigned integer at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        private uint UnbufferedReadUInt32()
        {
            this.br.BaseStream.Seek(this.GetRealPosition(this.position), SeekOrigin.Begin);
            return this.br.ReadUInt32();                        
        }

        /// <summary>
        /// Reads the signed short at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        public short ReadInt16(long virtualPosition)
        {
            return this.getShortFromBuffer(this.GetRealPosition(virtualPosition));
        }


        /// <summary>
        /// Original unbuffered method to read the signed short at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        private short UnbufferedReadInt16(long virtualPosition)
        {
            this.br.BaseStream.Seek(this.GetRealPosition(virtualPosition), SeekOrigin.Begin);
            return this.br.ReadInt16();            
        }

        /// <summary>
        /// Reads the signed short at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        public short ReadInt16()
        {
            return this.getShortFromBuffer(this.GetRealPosition(this.position));
        }

        /// <summary>
        /// Original unbuffered method to read the signed short at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        private short UnbufferedReadInt16()
        {
            this.br.BaseStream.Seek(this.GetRealPosition(this.position), SeekOrigin.Begin);
            return this.br.ReadInt16();            
        }

        /// <summary>
        /// Reads the signed integer at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        public int ReadInt32(long virtualPosition)
        {
            return this.getIntFromBuffer(this.GetRealPosition(virtualPosition));
        }

        /// <summary>
        /// Original unbuffered method to read the signed integer at the virtual position in the stream
        /// </summary>
        /// <param name="virtualPosition">Virtual position in the stream in voxels</param>
        /// <returns>Read value</returns>
        private int UnbufferedReadInt32(long virtualPosition)
        {
            this.br.BaseStream.Seek(this.GetRealPosition(virtualPosition), SeekOrigin.Begin);
            return this.br.ReadInt32();            
        }

        /// <summary>        
        /// Reads the signed integer at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        public int ReadInt32()
        {
            return this.getIntFromBuffer(this.GetRealPosition(this.position));
        }

        /// <summary>        
        /// Original unbuffered method to read the signed integer at the current position in the stream
        /// </summary>
        /// <returns>Read value</returns>
        private int UnbufferedReadInt32()
        {
            this.br.BaseStream.Seek(this.GetRealPosition(this.position), SeekOrigin.Begin);
            return this.br.ReadInt32();            
        }

        /// <summary>
        /// Reads <code>count</code> bytes from <code>offset</code> into <code>buffer</code>. Note:
        /// integer only positioning in the overridden method, so it may be unsafe to use this method. Use
        /// one of the other available Read methods to do this
        /// </summary>
        /// <param name="buffer">The target byte array</param>
        /// <param name="offset">The stream offset</param>
        /// <param name="count">The number of bytes to read</param>
        /// <returns></returns>
        public override int Read(byte[] buffer, int offset, int count)
        {
            for (int i = 0; i < count; i++)
            {
                buffer[i] = this.ReadByte(offset+i);                    
            }
            
            return 0;            
        }

        /// <summary>
        /// Sets the virtual position in the stream to <code>offset</code> if the origin is 
        /// <code>SeekOrigin.Begin</code>, the current position plus <code>offset</code> if 
        /// the origin is <code>SeekOrigin.Current</code> or the end minus <code>offset</code>
        /// if the origin is <code>SeekOrigin.End</code>, and returns the corresponding position 
        /// in the real stream.
        /// 
        /// Note: this may not be a traditional interpretation of <code>SeekOrigin.End</code>
        /// TODO: check this
        /// </summary>
        /// <param name="offset">The virtual offset</param>
        /// <param name="origin">The origin for <code>offset</code></param>
        /// <returns>Corresponding position in the real stream</returns>
        public override long Seek(long offset, SeekOrigin origin)
        {
            switch (origin)
            {
                case SeekOrigin.Begin:
                    return this.GetRealPosition(offset);
                case SeekOrigin.Current:
                    return this.GetRealPosition(offset + this.position);
                case SeekOrigin.End:
                    return this.GetRealPosition(this.length - offset);
            }
            throw new NotImplementedException(String.Format("SeekOrigin {0} is not understood", origin));
        }

        /// <summary>
        /// Overridden method to set the length of the stream. This is invalid in this virtual context
        /// </summary>
        /// <param name="value">The value for the length</param>
        public override void SetLength(long value)
        {
            throw new CtReaderException("The length of the stream cannot be set by the caller");
            // this.length = value;            
        }

        /// <summary>
        /// Overridden method to write to the stream. This is invalid in this virtual context
        /// </summary>
        /// <param name="buffer">The buffer to write</param>
        /// <param name="offset">The offset to use</param>
        /// <param name="count">The count of the bytes to use</param>
        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new CtReaderException("Cannot write to virtual CT reader stream");
        }        
    }
}
